package com.folderit.apps.rooms.sync;

import android.accounts.Account;
import android.accounts.AccountManager;
import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.AbstractThreadedSyncAdapter;
import android.content.ContentProviderClient;
import android.content.ContentResolver;
import android.content.ContentUris;
import android.content.ContentValues;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
import android.content.SyncRequest;
import android.content.SyncResult;
import android.content.res.Resources;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.Bundle;
import android.preference.PreferenceManager;
import android.support.v4.app.NotificationCompat;
import android.support.v4.app.TaskStackBuilder;
import android.util.Log;

import com.folderit.apps.rooms.MainActivity;
import com.folderit.apps.rooms.R;
import com.folderit.apps.rooms.Utility;
import com.folderit.apps.rooms.data.RoomsContract;
import com.folderit.apps.rooms.data.RoomsContract.ReservationEntry;
import com.folderit.apps.rooms.data.RoomsContract.RoomEntry;
import com.folderit.apps.rooms.data.RoomsContract.UserEntry;

import org.json.JSONArray;
import org.json.JSONException;
import org.json.JSONObject;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.net.HttpURLConnection;
import java.net.URL;
import java.util.ArrayList;
import java.util.Calendar;

public class RoomsSyncAdapter extends AbstractThreadedSyncAdapter {
    public final String LOG_TAG = RoomsSyncAdapter.class.getSimpleName();

    // Interval at which to sync with the ROOM, in milliseconds.
    // 60 seconds (1 minute) * 60 = 1 hour
    public static final int SYNC_INTERVAL = 60 * 60;
    public static final int SYNC_FLEXTIME = SYNC_INTERVAL/3;

    private static final String[] NOTIFY_ROOM_PROJECTION = new String[] {
            ReservationEntry.TABLE_NAME + "." +ReservationEntry._ID,
            RoomEntry.COLUMN_ROOM_NAME,
    };

    // these indices must match the projection
    private static final int INDEX_RESERVATION_ID = 0;
    private static final int INDEX_ROOM_NAME = 1;

    private static final int ROOM_NOTIFICATION_ID = 3004;

    private Context mContext;

    public RoomsSyncAdapter(Context context, boolean autoInitialize) {
        super(context, autoInitialize);

        mContext = context;
    }

    /**
     * Helper method to schedule the sync adapter periodic execution
     */
    public static void configurePeriodicSync(Context context, int syncInterval, int flexTime) {
        Account account = getSyncAccount(context);
        String authority = context.getString(R.string.content_authority);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            // we can enable inexact timers in our periodic sync
            SyncRequest request = new SyncRequest.Builder().
                    syncPeriodic(syncInterval, flexTime).
                    setSyncAdapter(account, authority).build();
            ContentResolver.requestSync(request);
        } else {
            ContentResolver.addPeriodicSync(account,
                    authority, new Bundle(), syncInterval);
        }
    }


    private static void onAccountCreated(Account newAccount, Context context) {
        /*
         * Since we've created an account
         */
        RoomsSyncAdapter.configurePeriodicSync(context, SYNC_INTERVAL, SYNC_FLEXTIME);

        /*
         * Without calling setSyncAutomatically, our periodic sync will not be enabled.
         */
        ContentResolver.setSyncAutomatically(newAccount, context.getString(R.string.content_authority), true);

        /*
         * Finally, let's do a sync to get things started
         */
        syncImmediately(context);
    }

    public static void initializeSyncAdapter(Context context) {
        getSyncAccount(context);
    }

    @Override
    public void onPerformSync(Account account, Bundle extras, String authority, ContentProviderClient provider, SyncResult syncResult) {

        Log.v(LOG_TAG, "Performing sync...");

        // These two need to be declared outside the try/catch
        // so that they can be closed in the finally block.
        HttpURLConnection urlConnection = null;
        BufferedReader reader = null;

        // Will contain the raw JSON response as a string.
        String roomsJsonStr = null;

        try {
            // Construct the URL
            final String BASE_URL =
                    "http://folderit.no-ip.info:9081/Reservations/getall";
            //final String DAYS_PARAM = "days";

            // I get the reservations for the next 15 days
            Uri builtUri = Uri.parse(BASE_URL);
                      /*.buildUpon()
                      .appendQueryParameter(DAYS_PARAM, "15")
                      .build();*/

            URL url = new URL(builtUri.toString());

            // Create the request
            urlConnection = (HttpURLConnection) url.openConnection();
            urlConnection.setRequestMethod("GET");
            urlConnection.connect();

            // Read the input stream into a String
            InputStream inputStream = urlConnection.getInputStream();
            StringBuffer buffer = new StringBuffer();
            if (inputStream == null) {
                // Nothing to do.
                return;
            }
            reader = new BufferedReader(new InputStreamReader(inputStream));

            String line;
            while ((line = reader.readLine()) != null) {
                // Since it's JSON, adding a newline isn't necessary (it won't affect parsing)
                // But it does make debugging a *lot* easier if you print out the completed
                // buffer for debugging.
                buffer.append(line + "\n");
            }

            if (buffer.length() == 0) {
                // Stream was empty.  No point in parsing.
                return;
            }
            roomsJsonStr = buffer.toString();
        } catch (IOException e) {
            Log.e(LOG_TAG, "Error ", e);
            return;
        } finally {
            if (urlConnection != null) {
                urlConnection.disconnect();
            }
            if (reader != null) {
                try {
                    reader.close();
                } catch (final IOException e) {
                    Log.e(LOG_TAG, "Error closing stream", e);
                }
            }
        }

        try {
            getReservationDataFromJson(roomsJsonStr);
            notifyNewReservation();

        } catch (JSONException e) {
            Log.e(LOG_TAG, e.getMessage(), e);
            e.printStackTrace();
        }

        // This will only happen if there was an error getting or parsing the forecast.
        return;
    }

    /**
     * Take the String representing the complete forecast in JSON Format and
     * pull out the data we need to construct the Strings needed for the wireframes.
     */
    private void getReservationDataFromJson(String roomsJsonStr)
            throws JSONException {

        // These are the names of the JSON objects that need to be extracted.

        // Rooms information
        final String ROOM_ID = "id";
        final String ROOM_NAME = "name";
        final String ROOM_DESC = "description";
        final String ROOM_RESERVATIONS = "reservations";
        final String ROOM_COLOR_ID = "color_id";

        // Reservation info. Each room has 0 or more reservations
        final String RESERVATION_ID = "id";
        final String RESERVATION_DESCRIPTION = "description";
        final String RESERVATION_BEGIN = "begin";
        final String RESERVATION_END = "end";
        final String RESERVATION_CREATED = "created";
        final String RESERVATION_USER = "user";

        // User info. Each room has a user
        final String USER_ID = "id";
        final String USER_NAME = "name";
        final String USER_PHONE_NUMBER = "phone_number";
        final String USER_EMAIL = "email";

        JSONArray roomArray = new JSONArray(roomsJsonStr);

        // These arrays will contain all the ids of the rooms and reservations
        // retrieved, so I will use them later for delete the rooms and reservations
        // that no exists anymore in the main app.
        ArrayList<Integer> existentRoomIds = new ArrayList<Integer>();
        ArrayList<Integer> existentReservationIds = new ArrayList<Integer>();

        for (int i = 0; i < roomArray.length(); i++)
        {
            JSONObject room = roomArray.getJSONObject(i);

            // Get the room fields
            String roomName = room.getString(ROOM_NAME);
            String roomDesc = room.getString(ROOM_DESC);
            int colorId = room.getInt(ROOM_COLOR_ID);
            int roomExternalId = room.getInt(ROOM_ID);

            // Add the external id to the existent rooms array
            existentRoomIds.add(roomExternalId);

            // Insert the room
            long roomId = addRoom(roomName, roomDesc, colorId, roomExternalId);

            // Get the reservations of the room
            JSONArray reservationArray = room.getJSONArray(ROOM_RESERVATIONS);

            for (int j = 0; j < reservationArray.length(); j++)
            {
                JSONObject reservation = reservationArray.getJSONObject(j);

                // First, get the user who creates the reservation
                JSONObject user = reservation.getJSONObject(RESERVATION_USER);
                String userName = user.getString(USER_NAME);
                String userPhoneNumber = user.getString(USER_PHONE_NUMBER);
                String userEmail = user.getString(USER_EMAIL);

                // Insert the user
                long userId = addUser(userName, userPhoneNumber, userEmail);

                // Now I have user and room ids, so I can insert the reservations.
                // Get the reservation fields.
                String reservationDesc = reservation.getString(RESERVATION_DESCRIPTION);
                String reservationBegin = reservation.getString(RESERVATION_BEGIN);
                String reservationEnd = reservation.getString(RESERVATION_END);
                String reservationCreated = reservation.getString(RESERVATION_CREATED);
                int reservationExternalId = reservation.getInt(RESERVATION_ID);

                // Add the reservation id to the existent reservation array
                existentReservationIds.add(reservationExternalId);

                // Insert the reservation
                ContentValues reservationValues = new ContentValues();
                reservationValues.put(ReservationEntry.COLUMN_BEGIN, reservationBegin);
                reservationValues.put(ReservationEntry.COLUMN_END, reservationEnd);
                reservationValues.put(ReservationEntry.COLUMN_EVENT_TITLE, reservationDesc);
                reservationValues.put(ReservationEntry.COLUMN_CREATED, reservationCreated);
                reservationValues.put(ReservationEntry.COLUMN_ROOM_KEY, roomId);
                reservationValues.put(ReservationEntry.COLUMN_USER_KEY, userId);
                reservationValues.put(ReservationEntry.COLUMN_EXTERNAL_ID, reservationExternalId);

                // TODO:
                // -Check if the reservation exists
                // -Bulk insert of new reservations
                // -Remove reservations that exist in the database but were delete (not contained
                //  in reservations array).
                addReservation(reservationValues);
            }
        }

        // First, remove old reservations
        deleteOldData();

        // Delete the rooms that not exist anymore
        deleteRemovedRooms(existentRoomIds);

        // Delete the reservations was removed in the main app.
        deleteRemovedReservations(existentReservationIds);
    }

    /**
     * Removes all the reservations which external ids is not
     * contained in the array passed as parameter.
     * @param existingIds External ids of existing reservations
     */
    private void deleteRemovedReservations(ArrayList<Integer> existingIds) {
        if (existingIds.size() < 1) {
            Log.v(LOG_TAG, "Not removed reservations to delete");
            return;
        }

        // builds the (1, 2, 3, 4) string
        String ids = buildWhereInString(existingIds);

        int removedReservations = getContext().getContentResolver().delete(
                ReservationEntry.CONTENT_URI,
                ReservationEntry.COLUMN_EXTERNAL_ID + " NOT IN " + ids,
                null
        );

        Log.v(LOG_TAG, removedReservations + " reservations have been removed " +
                "because they not exist anymore in the main app.");
    }

    private void deleteRemovedRooms(ArrayList<Integer> existingIds) {
        if (existingIds.size() < 1) {
            Log.v(LOG_TAG, "Not removed rooms to delete");
            return;
        }

        // builds the (1, 2, 3, 4) string
        String ids = buildWhereInString(existingIds);

        int removedRooms = getContext().getContentResolver().delete(
                RoomEntry.CONTENT_URI,
                RoomEntry.COLUMN_EXTERNAL_ID + " NOT IN " + ids,
                null
        );

        Log.v(LOG_TAG, removedRooms + " rooms have been removed because " +
                "they not exist anymore in the main app.");
    }

    /**
     * Deletes all the reservations which end time
     * older than yesterday.
     * */
    private void deleteOldData()
    {
        Calendar cal = Calendar.getInstance(); //Get's a calendar object with the current time.
        cal.add(Calendar.DATE, -1); //Signifies yesterday's date
        String yesterdayDate = RoomsContract.getDbDateString(cal.getTime());
        getContext().getContentResolver().delete(
                ReservationEntry.CONTENT_URI,
                ReservationEntry.COLUMN_END + " <= ?",
                new String[]{yesterdayDate});
    }

    private void addReservation(ContentValues reservationValues)
    {
        // Check if a reservation exists using the External ID
        Cursor cursor = mContext.getContentResolver().query(
                ReservationEntry.CONTENT_URI,
                new String[]{ ReservationEntry.TABLE_NAME + "." + ReservationEntry._ID },
                ReservationEntry.COLUMN_EXTERNAL_ID + " = ?",
                new String[]{ reservationValues.getAsString(ReservationEntry.COLUMN_EXTERNAL_ID) },
                null);

        if (cursor.moveToFirst()) {
            cursor.close();
            return;
        }

        Log.v(LOG_TAG, "Adding a new Reservation. Room: "
                + reservationValues.getAsString(ReservationEntry.COLUMN_ROOM_KEY)
                + " - From: " + reservationValues.getAsString(ReservationEntry.COLUMN_BEGIN)
                + " - To: " + reservationValues.getAsString(ReservationEntry.COLUMN_END));

        mContext.getContentResolver().insert(ReservationEntry.CONTENT_URI, reservationValues);
        cursor.close();
    }

    private long addRoom(String roomName, String roomDesc, int colorId, int externalId) {
        // TODO:
        // -Fields (name, description) could be updated. Maybe would be good to add a 'version'
        //  field so I can know if the room change, so I can update it in database.

        // First, check if a room with this name exists in the db
        Cursor cursor = mContext.getContentResolver().query(
                RoomEntry.CONTENT_URI,
                new String[]{ RoomEntry._ID },
                RoomEntry.COLUMN_EXTERNAL_ID + " = ?",
                new String[]{ Integer.toString(externalId) },
                null);

        if (cursor.moveToFirst()) {
            long roomId = cursor.getLong(cursor.getColumnIndex(RoomEntry._ID));
            cursor.close();
            return roomId;
        } else {
            Log.v(LOG_TAG, "Adding a new Room: " + roomName);

            ContentValues roomValues = new ContentValues();
            roomValues.put(RoomEntry.COLUMN_ROOM_NAME, roomName);
            roomValues.put(RoomEntry.COLUMN_DESCRIPTION, roomDesc);
            roomValues.put(RoomEntry.COLUMN_COLOR_ID, colorId);
            roomValues.put(RoomEntry.COLUMN_EXTERNAL_ID, externalId);

            Uri roomInsertUri = mContext.getContentResolver()
                    .insert(RoomEntry.CONTENT_URI, roomValues);

            cursor.close();
            return ContentUris.parseId(roomInsertUri);
        }
    }

    private long addUser(String name, String phoneNumber, String email) {
        // TODO:
        // -Fields (name, phone number) could be updated. Maybe would be good to add a 'version'
        //  field so I can know if the user change, so I can update it in database.

        // First, check if the user exists
        Cursor cursor = mContext.getContentResolver().query(
                UserEntry.CONTENT_URI,
                new String[]{ UserEntry._ID },
                UserEntry.COLUMN_EMAIL + " = ?",
                new String[]{ email },
                null);

        if (cursor.moveToFirst()) {
            long userId = cursor.getLong(cursor.getColumnIndex(UserEntry._ID));
            cursor.close();
            return userId;
        } else {
            Log.v(LOG_TAG, "Adding a new User: " + name);

            ContentValues userValues = new ContentValues();
            userValues.put(UserEntry.COLUMN_NAME, name);
            userValues.put(UserEntry.COLUMN_PHONE_NUMBER, phoneNumber);
            userValues.put(UserEntry.COLUMN_EMAIL, email);

            Uri userInsertUri = mContext.getContentResolver()
                    .insert(UserEntry.CONTENT_URI, userValues);

            cursor.close();
            return ContentUris.parseId(userInsertUri);
        }
    }

    /**
     * Helper method to have the sync adapter sync immediately
     * @param context The context used to access the account service
     */
    public static void syncImmediately(Context context) {
        Bundle bundle = new Bundle();
        bundle.putBoolean(ContentResolver.SYNC_EXTRAS_EXPEDITED, true);
        bundle.putBoolean(ContentResolver.SYNC_EXTRAS_MANUAL, true);
        ContentResolver.requestSync(getSyncAccount(context),
                context.getString(R.string.content_authority), bundle);
    }

    /**
     * Helper method to get the fake account to be used with SyncAdapter, or make a new one
     * if the fake account doesn't exist yet.  If we make a new account, we call the
     * onAccountCreated method so we can initialize things.
     *
     * @param context The context used to access the account service
     * @return a fake account.
     */
    public static Account getSyncAccount(Context context) {
        // Get an instance of the Android account manager
        AccountManager accountManager =
                (AccountManager) context.getSystemService(Context.ACCOUNT_SERVICE);

        // Create the account type and default account
        Account newAccount = new Account(
                context.getString(R.string.app_name), context.getString(R.string.sync_account_type));

        // If the password doesn't exist, the account doesn't exist
        if ( null == accountManager.getPassword(newAccount) ) {

        /*
         * Add the account and account type, no password or user data
         * If successful, return the Account object, otherwise report an error.
         */
            if (!accountManager.addAccountExplicitly(newAccount, "", null)) {
                return null;
            }
            /*
             * If you don't set android:syncable="true" in
             * in your <provider> element in the manifest,
             * then call ContentResolver.setIsSyncable(account, AUTHORITY, 1)
             * here.
             */

            onAccountCreated(newAccount, context);
        }
        return newAccount;
    }

    private void notifyNewReservation() {
        Context context = getContext();

        // If the notifications are disabled, there is nothing to do
        if (!Utility.notificationsEnabled(context))
            return;

        //checking the last reservation notified
        SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
        String lastNotificationKey = context.getString(R.string.pref_last_notification);

        long lastReservationNotified = prefs.getLong(lastNotificationKey, 0);

        // We'll ask for all reservations
        Uri reservationUri = ReservationEntry.CONTENT_URI;

        // we'll query contentProvider asking for all the not notified reservations
        Cursor cursor = context.getContentResolver().query(
                reservationUri, NOTIFY_ROOM_PROJECTION,
                ReservationEntry.TABLE_NAME + "." + ReservationEntry._ID + " > ?",
                new String[] { Long.toString(lastReservationNotified) },
                ReservationEntry.TABLE_NAME + "." + ReservationEntry._ID + " DESC");

        int newReservationQty = cursor.getCount();

        if (newReservationQty < 1) {
            cursor.close();
            return;
        }

        if (cursor.moveToFirst()) {
            long notificationId = cursor.getLong(INDEX_RESERVATION_ID);

            // Title of the notification
            String title = context.getString(R.string.app_name);

            // Message of the notifications using plurals
            Resources res = getContext().getResources();
            String message = res.getQuantityString(
                    R.plurals.numberOfNewReservations,
                    newReservationQty,
                    newReservationQty);

            // setAutoCancel(true) eliminates the notification when user clicks
            NotificationCompat.Builder mBuilder =
                    new NotificationCompat.Builder(getContext())
                            .setSmallIcon(R.drawable.ic_launcher)
                            .setAutoCancel(true)
                            .setContentTitle(title)
                            .setContentText(message);

            // Creates an explicit intent for an Activity in your app
            // For now, I always open the main activity.
            Intent resultIntent = new Intent(getContext(), MainActivity.class);

            // The stack builder object will contain an artificial back stack for the
            // started Activity.
            // This ensures that navigating backward from the Activity leads out of
            // your application to the Home screen.
            TaskStackBuilder stackBuilder = TaskStackBuilder.create(getContext());

            // Adds the back stack for the Intent (but not the Intent itself)
            stackBuilder.addParentStack(MainActivity.class);

            // Adds the Intent that starts the Activity to the top of the stack
            stackBuilder.addNextIntent(resultIntent);
            PendingIntent resultPendingIntent =
                    stackBuilder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT);

            mBuilder.setContentIntent(resultPendingIntent);
            NotificationManager mNotificationManager =
                    (NotificationManager) mContext.getSystemService(Context.NOTIFICATION_SERVICE);

            // mId allows you to update the notification later on.
            mNotificationManager.notify(ROOM_NOTIFICATION_ID, mBuilder.build());

            // Update the last notified reservation
            SharedPreferences.Editor editor = prefs.edit();
            editor.putLong(lastNotificationKey, notificationId);
            editor.commit();
        }

        cursor.close();
    }

    private String buildWhereInString(ArrayList<Integer> ids) {
        StringBuilder inQuery = new StringBuilder();

        inQuery.append("(");
        boolean first = true;
        for (int id : ids) {
            if (first) {
                first = false;
                inQuery.append(id);
            } else {
                inQuery.append(", ").append(id);
            }
        }
        inQuery.append(")");

        return inQuery.toString();
    }
}
